// See LICENSE for license details.

package chisel3.internal

import scala.collection.mutable.ArrayBuffer

import chisel3.core._

class ChiselException(message: String, cause: Throwable) extends Exception(message, cause)

private[chisel3] object throwException {
  def apply(s: String, t: Throwable = null): Nothing =
    throw new ChiselException(s, t)
}

/** Records and reports runtime errors and warnings. */
private[chisel3] class ErrorLog {
  def hasErrors: Boolean = errors.exists(_.isFatal)

  /** Log an error message */
  def error(m: => String): Unit =
    errors += new Error(m, getUserLineNumber)

  /** Log a warning message */
  def warning(m: => String): Unit =
    errors += new Warning(m, getUserLineNumber)

  /** Log a deprecation warning message */
  def deprecated(m: => String): Unit =
    errors += new DeprecationWarning(m, getUserLineNumber)

  /** Emit an informational message */
  def info(m: String): Unit =
    println(new Info("[%2.3f] %s".format(elapsedTime/1e3, m), None))  // scalastyle:ignore regex

  /** Prints error messages generated by Chisel at runtime. */
  def report(): Unit = errors foreach println  // scalastyle:ignore regex

  /** Throw an exception if any errors have yet occurred. */
  def checkpoint(): Unit = {
    if(hasErrors) {
      import Console._
      throwException(errors.map(_ + "\n").reduce(_ + _) +
        UNDERLINED + "CODE HAS " + errors.filter(_.isFatal).length + RESET +
        UNDERLINED + " " + RED + "ERRORS" + RESET +
        UNDERLINED + " and " + errors.filterNot(_.isFatal).length + RESET +
        UNDERLINED + " " + YELLOW + "WARNINGS" + RESET)
    } else {
      // No fatal errors. Report accumulated warnings and clear them.
      report()
      errors.clear()
    }
  }

  private def findFirstUserFrame(stack: Array[StackTraceElement]): Option[StackTraceElement] = {
    def isUserCode(ste: StackTraceElement): Boolean = {
      def isUserModule(c: Class[_]): Boolean =
        c != null && (c == classOf[UserModule] || isUserModule(c.getSuperclass))
      isUserModule(Class.forName(ste.getClassName))
    }

    stack.indexWhere(isUserCode) match {
      case x if x < 0 => None
      case x => Some(stack(x))
    }
  }

  private def getUserLineNumber =
    findFirstUserFrame(Thread.currentThread().getStackTrace)

  private val errors = ArrayBuffer[LogEntry]()

  private val startTime = System.currentTimeMillis
  private def elapsedTime: Long = System.currentTimeMillis - startTime
}

private abstract class LogEntry(msg: => String, line: Option[StackTraceElement]) {
  def isFatal: Boolean = false
  def format: String

  override def toString: String = line match {
    case Some(l) => s"${format} ${l.getFileName}:${l.getLineNumber}: ${msg} in class ${l.getClassName}"
    case None => s"${format} ${msg}"
  }

  protected def tag(name: String, color: String): String =
    s"[${color}${name}${Console.RESET}]"
}

private class Error(msg: => String, line: Option[StackTraceElement]) extends LogEntry(msg, line) {
  override def isFatal: Boolean = true
  def format: String = tag("error", Console.RED)
}

private class Warning(msg: => String, line: Option[StackTraceElement]) extends LogEntry(msg, line) {
  def format: String = tag("warn", Console.YELLOW)
}

private class DeprecationWarning(msg: => String, line: Option[StackTraceElement]) extends LogEntry(msg, line) {
  def format: String = tag("warn", Console.CYAN)
}

private class Info(msg: => String, line: Option[StackTraceElement]) extends LogEntry(msg, line) {
  def format: String = tag("info", Console.MAGENTA)
}
